/*
 * Copyright (c) 2009-2021 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.font;

import com.jme3.font.BitmapFont.Align;
import com.jme3.font.BitmapFont.VAlign;
import com.jme3.font.ColorTags.Range;
import com.jme3.math.ColorRGBA;

import java.util.LinkedList;

/**
 * Manage and align LetterQuads
 * @author YongHoon
 */
class Letters {
    private final LetterQuad head;
    private final LetterQuad tail;
    private final BitmapFont font;
    private LetterQuad current;
    private final StringBlock block;
    private float totalWidth;
    private float totalHeight;
    private final ColorTags colorTags = new ColorTags();
    private ColorRGBA baseColor = null;
    private float baseAlpha = -1;
    private String plainText;

    Letters(BitmapFont font, StringBlock bound, boolean rightToLeft) {
        final String text = bound.getText();
        this.block = bound;
        this.font = font;
        head = new LetterQuad(font, rightToLeft);
        tail = new LetterQuad(font, rightToLeft);
        setText(text);
    }

    void setText(final String text) {
        colorTags.setText(text);
        plainText = colorTags.getPlainText();

        head.setNext(tail);
        tail.setPrevious(head);
        current = head;
        if (text != null && plainText.length() > 0) {
            LetterQuad l = head;
            CharSequence characters = plainText;
            if (font.getGlyphParser() != null) {
                characters = font.getGlyphParser().parse(plainText);
            }

            for (int i = 0; i < characters.length(); i++) {
                l = l.addNextCharacter(characters.charAt(i));
                if (baseColor != null) {
                    // Give the letter a default color if
                    // one has been provided.
                    l.setColor(baseColor);
                }
            }
        }

        LinkedList<Range> ranges = colorTags.getTags();
        if (!ranges.isEmpty()) {
            for (int i = 0; i < ranges.size()-1; i++) {
                Range start = ranges.get(i);
                Range end = ranges.get(i+1);
                setColor(start.start, end.start, start.color);
            }
            Range end = ranges.getLast();
            setColor(end.start, plainText.length(), end.color);
        }

        invalidate();
    }

    LetterQuad getHead() {
        return head;
    }

    LetterQuad getTail() {
        return tail;
    }

    void update() {
        LetterQuad l = head;
        int lineCount = 1;
        BitmapCharacter ellipsis = font.getCharSet().getCharacter(block.getEllipsisChar());
        float ellipsisWidth = ellipsis!=null? ellipsis.getWidth()*getScale(): 0;

        while (!l.isTail()) {
            if (l.isInvalid()) {
                l.update(block);
                // Without a text block, the next line always returns false = no text wrap will be applied.
                if (l.isInvalid(block)) {
                    switch (block.getLineWrapMode()) {
                    case Character:
                        lineWrap(l);
                        lineCount++;
                        break;
                    case Word:
                        if (!l.isBlank()) {
                            // search last blank character before this word
                            LetterQuad blank = l;
                            while (!blank.isBlank()) {
                                if (blank.isLineStart() || blank.isHead()) {
                                    lineWrap(l);
                                    lineCount++;
                                    blank = null;
                                    break;
                                }
                                blank = blank.getPrevious();
                            }
                            if (blank != null) {
                                blank.setEndOfLine();
                                lineCount++;
                                while (blank != l) {
                                    blank = blank.getNext();
                                    blank.invalidate();
                                    blank.update(block);
                                }
                            }
                        }
                        break;
                    case NoWrap:
                        LetterQuad cursor = l.getPrevious();
                        while (cursor.isInvalid(block, ellipsisWidth) && !cursor.isLineStart()) {
                            cursor = cursor.getPrevious();
                        }
                        cursor.setBitmapChar(ellipsis);
                        cursor.update(block);
                        cursor = cursor.getNext();
                        while (!cursor.isTail() && !cursor.isLineFeed()) {
                            cursor.setBitmapChar(null);
                            cursor.update(block);
                            cursor = cursor.getNext();
                        }
                        break;
                    case Clip:
                        // Clip the character that falls out of bounds
                        l.clip(block);

                        // Clear the rest up to the next line feed.
                        // = for texts attached to a text block, all coming characters are cleared except a linefeed is explicitly used
                        for (LetterQuad q = l.getNext(); !q.isTail() && !q.isLineFeed(); q = q.getNext()) {
                            q.setBitmapChar(null);
                            q.update(block);
                        }
                        break;
                    }
                }
            } else if (current.isInvalid(block)) {
                invalidate(current);
            }
            if (l.isEndOfLine()) {
                lineCount++;
            }
            l = l.getNext();
        }

        block.setLineCount(lineCount);
        align();
        rewind();
    }

    private void align() {
        if (block.getTextBox() == null) {
            // Without a text block, there is no alignment.
            return;

            // For unbounded left-to-right texts the letters will simply be shown starting from
            // x0 = 0 and advance toward right as line length is considered to be infinite.
            // For unbounded right-to-left texts the letters will be shown starting from x0 = 0
            // (at the same position as left-to-right texts) but move toward the left from there.
        }

        final Align alignment = block.getAlignment();
        final VAlign valignment = block.getVerticalAlignment();
        final float width = block.getTextBox().width;
        final float height = block.getTextBox().height;
        float lineWidth = 0;
        float gapX = 0;
        float gapY = 0;

        validateSize();
        if (totalHeight < height) { // align vertically only for no overflow
            switch (valignment) {
                case Top:
                    gapY = 0;
                    break;
                case Center:
                    gapY = (height - totalHeight) * 0.5f;
                    break;
                case Bottom:
                    gapY = height - totalHeight;
                    break;
            }
        }

        if (font.isRightToLeft()) {
            if ((alignment == Align.Right && valignment == VAlign.Top)) {
                return;
            }
            LetterQuad cursor = tail.getPrevious();
            // Temporary set the flag, it will be reset when invalidated.
            cursor.setEndOfLine();
            while (!cursor.isHead()) {
                if (cursor.isEndOfLine()) {
                    if (alignment == Align.Left) {
                        gapX = block.getTextBox().x - cursor.getX0();
                    } else if (alignment == Align.Center) {
                        gapX = (block.getTextBox().x - cursor.getX0()) / 2;
                    } else {
                        gapX = 0;
                    }
                }
                cursor.setAlignment(gapX, gapY);
                cursor = cursor.getPrevious();
            }
        } else { // left-to-right
            if (alignment == Align.Left && valignment == VAlign.Top) {
                return;
            }
            LetterQuad cursor = tail.getPrevious();
            // Temporary set the flag, it will be reset when invalidated.
            cursor.setEndOfLine();
            while (!cursor.isHead()) {
                if (cursor.isEndOfLine()) {
                    lineWidth = cursor.getX1() - block.getTextBox().x;
                    if (alignment == Align.Center) {
                        gapX = (width - lineWidth) / 2;
                    } else if (alignment == Align.Right) {
                        gapX = width - lineWidth;
                    } else {
                        gapX = 0;
                    }
                }
                cursor.setAlignment(gapX, gapY);
                cursor = cursor.getPrevious();
            }
        }
    }

    private void lineWrap(LetterQuad l) {
        if (l.isHead() || l.isBlank())
            return;
        l.getPrevious().setEndOfLine();
        l.invalidate();
        l.update(block);
    }

    float getCharacterX0() {
        return current.getX0();
    }

    float getCharacterY0() {
        return current.getY0();
    }

    float getCharacterX1() {
        return current.getX1();
    }

    float getCharacterY1() {
        return current.getY1();
    }

    float getCharacterAlignX() {
        return current.getAlignX();
    }

    float getCharacterAlignY() {
        return current.getAlignY();
    }

    float getCharacterWidth() {
        return current.getWidth();
    }

    float getCharacterHeight() {
        return current.getHeight();
    }

    public boolean nextCharacter() {
        if (current.isTail())
            return false;
        current = current.getNext();
        return true;
    }

    public int getCharacterSetPage() {
        return current.getBitmapChar().getPage();
    }

    public LetterQuad getQuad() {
        return current;
    }

    public void rewind() {
        current = head;
    }

    public void invalidate() {
        invalidate(head);
    }

    public void invalidate(LetterQuad cursor) {
        totalWidth = -1;
        totalHeight = -1;

        while (!cursor.isTail() && !cursor.isInvalid()) {
            cursor.invalidate();
            cursor = cursor.getNext();
        }
    }

    float getScale() {
        return block.getSize() / font.getCharSet().getRenderedSize();
    }

    public boolean isPrintable() {
        return current.getBitmapChar() != null;
    }

    float getTotalWidth() {
        validateSize();
        return totalWidth;
    }

    float getTotalHeight() {
        validateSize();
        return totalHeight;
    }

    void validateSize() {
        // also called from BitMaptext.getLineWidth() via getTotalWidth()
        if (totalWidth < 0) {
            LetterQuad l = head;
            while (!l.isTail()) {
                if (font.isRightToLeft()) {
                    totalWidth = Math.max(totalWidth, Math.abs(l.getX0()));
                } else {
                    totalWidth = Math.max(totalWidth, l.getX1());
                }
                l = l.getNext();
            }
        }
        totalHeight = font.getLineHeight(block) * block.getLineCount();
    }

    /**
     * @param start start index to set style. inclusive.
     * @param end   end index to set style. EXCLUSIVE.
     * @param style
     */
    void setStyle(int start, int end, int style) {
        LetterQuad cursor = head.getNext();
        while (!cursor.isTail()) {
            if (cursor.getIndex() >= start && cursor.getIndex() < end) {
                cursor.setStyle(style);
            }
            cursor = cursor.getNext();
        }
    }

    /**
     * Sets the base color for all new letter quads and resets
     * the color of existing letter quads.
     */
    void setColor(ColorRGBA color) {
        baseColor = color;
        colorTags.setBaseColor(color);
        setColor(0, block.getText().length(), color);
    }

    ColorRGBA getBaseColor() {
        return baseColor;
    }

    /**
     * @param start start index to set style. inclusive.
     * @param end   end index to set style. EXCLUSIVE.
     * @param color
     */
    void setColor(int start, int end, ColorRGBA color) {
        LetterQuad cursor = head.getNext();
        while (!cursor.isTail()) {
            if (cursor.getIndex() >= start && cursor.getIndex() < end) {
                cursor.setColor(color);
            }
            cursor = cursor.getNext();
        }
    }

    float getBaseAlpha() {
        return baseAlpha;
    }

    void setBaseAlpha(float alpha) {
        this.baseAlpha = alpha;
        colorTags.setBaseAlpha(alpha);

        if (alpha == -1) {
            alpha = baseColor != null ? baseColor.a : 1;
        }

        // Forward the new alpha to the letter quads
        LetterQuad cursor = head.getNext();
        while (!cursor.isTail()) {
            cursor.setAlpha(alpha);
            cursor = cursor.getNext();
        }

        // If the alpha was reset to "default" (-1),
        // then the color tags are potentially reset, and
        // we need to reapply them.  This has to be done
        // second since it may override any alpha values
        // set above... but you still need to do the above
        // since non-color tagged text is treated differently
        // even if part of a color tagged string.
        if (baseAlpha == -1) {
            LinkedList<Range> ranges = colorTags.getTags();
            if (!ranges.isEmpty()) {
                for (int i = 0; i < ranges.size()-1; i++) {
                    Range start = ranges.get(i);
                    Range end = ranges.get(i+1);
                    setColor(start.start, end.start, start.color);
                }
                Range end = ranges.getLast();
                setColor(end.start, plainText.length(), end.color);
            }
        }

        invalidate();
    }

}
